package com.google.code.swingonfire.component.clock;

import com.google.code.swingonfire.utility.Utils;

/**
 * Describes the state of on hand of the clock. Allows animation.
 * 
 * @author thred
 */
public class JClockHandState
{
    private static final double SPEEDUP = 10;

    /**
     * The type of the hand
     */
    private final JClockHandType type;

    /**
     * The value of the hand, that should be achieved when the animation has finished.
     */
    private double target;

    /**
     * The current value of the hand, changes with the animation until the target value is reached.
     */
    private double current;

    /**
     * An override for the current and target values. If this value is set, the hand points in that direction. This is
     * used, e.g. if the user grabs an hand with the mouse.
     */
    private Double override = null;

    private double animateChangePerSecond;
    private boolean locked = true;
    private double targetDiff;
    private boolean selected = false;

    public JClockHandState(JClockHandType type)
    {
        super();

        this.type = type;
    }

    public JClockHandType getType()
    {
        return type;
    }

    public double getMaximum()
    {
        return type.getMaximum();
    }

    public double getMaximumChangePerSecond()
    {
        return type.getMaximumChangePerSecond();
    }

    public double getRoundToNearest()
    {
        return type.getRoundToNearest();
    }

    public double getRoundOffset()
    {
        return type.getRoundOffset();
    }

    public double getTickDistance()
    {
        return type.getTickDistance();
    }

    public double getLockDistance()
    {
        return type.getLockDistance();
    }

    public boolean isAnimateYearOnAdjusting()
    {
        return type.isAnimateYearOnAdjusting();
    }

    public boolean isAnimateMonthOnAdjusting()
    {
        return type.isAnimateMonthOnAdjusting();
    }

    public boolean isAnimateDayOnAdjusting()
    {
        return type.isAnimateDayOnAdjusting();
    }

    public boolean isAnimateSunOnAdjusting()
    {
        return type.isAnimateSunOnAdjusting();
    }

    public boolean isAnimateAMPMOnAdjusting()
    {
        return type.isAnimateAMPMOnAdjusting();
    }

    public boolean isAnimateHourOnAdjusting()
    {
        return type.isAnimateHourOnAdjusting();
    }

    public boolean isAnimateMinuteOnAdjusting()
    {
        return type.isAnimateMinuteOnAdjusting();
    }

    public boolean isAnimateSecondOnAdjusting()
    {
        return type.isAnimateSecondOnAdjusting();
    }

    /**
     * Converts the value to fit into the specified range. It's like an angle, which should be something between 0 and
     * 360.
     * 
     * @param value the value
     * @return the bounded value
     */
    public double bound(double value)
    {
        while (value > getMaximum())
        {
            value -= getMaximum();
        }

        while (value < 0)
        {
            value += getMaximum();
        }

        return value;
    }

    /**
     * Rounds the value to the specified rouding
     * 
     * @param value the value
     * @return the rounded value
     */
    public double round(double value)
    {
        return Utils.round(bound(value), getRoundToNearest(), getRoundOffset());
    }

    public double getTarget()
    {
        return target;
    }

    protected void setTarget(double target)
    {
        this.target = target;
    }

    public double getCurrent()
    {
        return current;
    }

    protected void setCurrent(double current)
    {
        this.current = current;
    }

    public Double getOverride()
    {
        return override;
    }

    public void setOverride(Double override)
    {
        this.override = override;
    }

    public double getAnimateChangePerSecond()
    {
        return animateChangePerSecond;
    }

    protected void setAnimateChangePerSecond(double animateChangePerSecond)
    {
        this.animateChangePerSecond = animateChangePerSecond;
    }

    public double getTargetDiff()
    {
        return targetDiff;
    }

    protected void setTargetDiff(double targetDiff)
    {
        this.targetDiff = targetDiff;
    }

    public void setSelected(boolean selected)
    {
        this.selected = selected;
    }

    public boolean isSelected()
    {
        return selected;
    }

    public boolean set(double value, boolean animate)
    {
        return set(value, 0, animate);
    }

    /**
     * Sets the value of this hand.
     * @param value the desired value
     * @param rotation the rotation direction (-1/1), or 0 for undetermined 
     * @param animate true to animate
     * @return true if a new value was set
     */
    public boolean set(double value, int rotation, boolean animate)
    {
//        System.err.printf("type=%12s, value=%,10.3f, rotation=%3d, animate=%5b\n", type, value, rotation, animate);
        
        if (override != null)
        {
            value = override.doubleValue();
        }
        else
        {
            value = round(value);
        }

        if ((Math.abs(value - getTarget()) < getLockDistance()) && (Math.abs(value - getCurrent()) < getLockDistance()))
        {
            return false;
        }

        if (!animate)
        {
            target = current = value;

            return true;
        }

        if ((!locked) && (animateChangePerSecond != 0))
        {
            rotation = (animateChangePerSecond > 0) ? 1 : -1;
        }
        else if (rotation == 0)
        {
            double boundedCurrent = bound(current);

            if (value > boundedCurrent)
            {
                double diff = value - boundedCurrent;

                rotation = (diff > (getMaximum() / 2)) ? -1 : 1;
            }
            else
            {
                double diff = value - boundedCurrent;

                rotation = (diff < (-getMaximum() / 2)) ? 1 : -1;
            }
        }

        target = value;
        locked = false;

        if (rotation > 0)
        {
            while (current > target)
            {
                current -= getMaximum();
            }

            for (int i = 1; i < rotation; i += 1)
            {
                current -= getMaximum();
            }
        }
        else
        {
            while (current < target)
            {
                current += getMaximum();
            }

            for (int i = -1; i > rotation; i -= 1)
            {
                current += getMaximum();
            }
        }

        targetDiff = target - current;

        return true;
    }

    public boolean update(double delay)
    {
        if (Math.abs(current - target) < getLockDistance())
        {
            return false;
        }

        if (targetDiff > 0)
        {
            if (!locked)
            {
                // accelerating
                if (animateChangePerSecond < 0)
                {
                    animateChangePerSecond = -animateChangePerSecond;
                }

                animateChangePerSecond += getMaximumChangePerSecond() * SPEEDUP * delay;

                if (animateChangePerSecond > getMaximumChangePerSecond())
                {
                    animateChangePerSecond = getMaximumChangePerSecond();
                }

                current += animateChangePerSecond * delay;

                if (current >= target)
                {
                    current = target + ((getTickDistance() / getMaximumChangePerSecond()) * animateChangePerSecond);
                    animateChangePerSecond = 0;
                    locked = true;
                }
            }
            else
            {
                current = target - ((current - target) / 2);

                if (Math.abs(current - target) < getLockDistance())
                {
                    animateChangePerSecond = 0;
                    locked = true;
                }
            }
        }
        else
        {
            if (!locked)
            {
                // accelerating
                if (animateChangePerSecond > 0)
                {
                    animateChangePerSecond = -animateChangePerSecond;
                }

                animateChangePerSecond -= getMaximumChangePerSecond() * SPEEDUP * delay;

                if (animateChangePerSecond < -getMaximumChangePerSecond())
                {
                    animateChangePerSecond = -getMaximumChangePerSecond();
                }

                current += animateChangePerSecond * delay;

                if (current <= target)
                {
                    current = target - ((getTickDistance() / getMaximumChangePerSecond()) * -animateChangePerSecond);
                    animateChangePerSecond = 0;
                    locked = true;
                }
            }
            else
            {
                current = target - ((current - target) / 2);

                if (Math.abs(current - target) < getLockDistance())
                {
                    animateChangePerSecond = 0;
                    locked = true;
                }
            }
        }

        return true;
    }

}
